电赛预热—电源软件如何准备?
击上方“果果小师弟”,选择“置顶/星标公众号”
干货福利,第一时间送达!
摘要:PWM和SPWM在电源的备战中是很有必要的。基础的恒流源、恒压源需要使用PWM的占空比及频率来达到数控的作用,往后的逆变则需要用到SPWM。当然还有ADC、DAC、IIC、SPI、算法。
一、PWM/SPWM
脉冲宽度调制(PWM),是英文“Pulse Width Modulation”的缩写,简称脉宽调试。是利用微处理器的数字输出来对模拟电路进行控制的一种非常有效的技术。广泛应用在从测量、通信到功率控制与变换的许多领域中。
SPWM(Sinusoidal PWM) 法是一种比较成熟的、使用较广泛的PWM法。冲量相等而形状不同的窄脉冲加在具有惯性的环节上时,其效果基本相同。SPWM法就是以该结论为理论基础,用脉冲宽度按正弦规律变化而和正弦波等效的PWM波形即SPWM波形控制逆变电路中开关器件的通断,使其输出的脉冲电压的面积与所希望输出的正弦波在相应区间内的面积相等,通过改变调制波的频率和幅值则可调节逆变电路输出电压的频率和幅值。
1、CubeMX相关配置-PWM
使能PWM通道
在这里我将TIM2的Channel1设置为PWM输出通道(PWM Generation CHx 正向、PWM Generation CHxN 反向、PWM Generation CHx CHxN一对互补pwm输出)
配置频率及占空比
频率 = 定时器时钟 / (Prescaler 预分频 + 1)/ (Counter Period 计数值 + 1)Hz
占空比 = Pulse ( 对比值) / (C ounter Period 计数值)%
2、编写业务代码-PWM
// 使能timx的通道y
HAL_TIM_PWM_Start(&htimx,TIM_CHANNEL_y);
// 修改timx的通道y的pwm比较值为z,即修改占空比
__HAL_TIM_SET_COMPARE(&htimx, TIM_CHANNEL_y, z);
pwm 的输出是很简单的,但是因为定时器的频率是有上限的通常需要在频率和pwm
的精细度两者之间做取舍。所以你想做电源,那么你可以了解一下STM32F334
这款处理器,它拥有一个高分辨率定时器 (HRTIM),能将定时器的频率倍频至4.096G
。那你在频率和pwm
的精细度两者都可以兼得。
SPWM 其实就是在 PWM 的基础上,让 PWM 的占空比做正弦变化。
3、CubeMX相关配置-SPWM
之前的 PWM 生成的操作不变,只需要开启一个新的定时器,配置完后需要开启定时器中断
4、使用软件生成正弦向量表-SPWM
SPWM 中值 = Pulse ( 对比值) /2SPWM 幅值 = Pulse ( 对比值) /2
周内点数影响频率与正弦波精细度。周内点数越大,频率越小、正弦波精细度越高。
5、编写业务代码-SPWM
uint16_t sin[] =
{
1800,1913,2025,2137,2247,2356,2462,2566,2667,2764,
2858,2947,3032,3112,3186,3256,3319,3377,3428,3473,
3511,3543,3568,3585,3596,3600,3596,3585,3568,3543,
3511,3473,3428,3377,3319,3256,3186,3112,3032,2947,
2858,2764,2667,2566,2462,2356,2247,2137,2025,1913,
1800,1686,1574,1462,1352,1243,1137,1033,932,835,
741,652,567,487,413,343,280,222,171,126,
88,56,31,14,3,0,3,14,31,56,
88,126,171,222,280,343,413,487,567,652,
741,835,932,1033,1137,1243,1352,1462,1574,1686
}
int main()
{
HAL_TIM_PWM_Start(&htimx,TIM_CHANNEL_y); // 开启pwm输出
HAL_TIM_Base_Start_IT(&htimz); //使能刚刚配置的定时器z
while(1)
{
}
}
/**
* @brief 定时器中断的回调函数
* @param htim 触发中断的定时器
* @retval None
*/
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
static int i = 0;
if(++i == size)i = 0;
if (htim->Instance == htim3.Instance)
{
__HAL_TIM_SET_COMPARE(&htimx, TIM_CHANNEL_y, sin[i]); //由向量表修改占空比
}
}
二、ADC/SDADC/ADS
先介绍最简单的片上 ADC,通常是 12 位,精度则为 3.3/4096 v。读取 ADC 的方式有很多:
1、轮询
2、中断
3、DMA
因为在实际开发中仅有轮询和 DMA 存在使用场景,所以在这里我仅介绍轮询和DMA 的方式。
1、CubeMX相关配置-轮询方式
2、编写业务代码-轮询方式
while(1)
{
HAL_ADC_Start(&hadc1);//启动ADC装换
HAL_ADC_PollForConversion(&hadc1, 50);//等待转换完成,第二个参数表示超时时间,
单位ms.
if(HAL_IS_BIT_SET(HAL_ADC_GetState(&hadc1), HAL_ADC_STATE_REG_EOC))
{
AD_Value = HAL_ADC_GetValue(&hadc1);//读取ADC转换数据,数据为12位
printf("[\tmain]info:v=%.1fmv\r\n",AD_Value*3300.0/4096);//打印日志
}
}
前面介绍了通过 ADC 轮询的方式采集单通道的数据。现在介绍一下通过 DMA 方式采集多通道的数据。
3、CubeMX相关配置-DMA方式
3.1初始化两个ADC通道
3.2、配置相关属性
使能扫描转换模式 (Scan Conversion Mode), 使能连续转换模式 (Continuous Conversion Mode)。
ADC 规则组选择转换通道数为 2(Number Of Conversion)。
配置 Rank 的输入通道。
3.3、添加 DMA
添加DMA设置,设置为连续传输模式,数据长度为字。
4、编写业务代码-DMA 方式
1、在main 函数前面添加变量。其中 ADC_Value 作为转换数据缓存数组,ad1,ad2 存储PA0(转换通道 0),PA1(转换通道1) 的电压值。
/* USER CODE BEGIN PV */
/* Private variables */
uint32_t ADC_Value[100];
uint8_t i;
uint32_t ad1,ad2;
/* USER CODE END PV */
2、在 while(1) 前面以 DMA 方式开启 ADC 装换。HAL_ADC_Start_DMA()
函数第二个参数为数据存储起始地址,第三个参数为 DMA 传输数据的长度。
/* USER CODE BEGIN 2 */
HAL_ADC_Start_DMA(&hadc1, (uint32_t*)&ADC_Value, 100);
/* USER CODE END 2 */
由于DMA
采用了连续传输的模式,ADC
采集到的数据会不断传到到存储器中(此处即为数组 ADC_Value
)。ADC
采集的数据从 ADC_Value[0]
一直存储到 ADC_Value[99]
,然后采集到的数据又重新存储到ADC_Value[0]
,一直到ADC_Value[99]
。所以ADC_Value
数组里面的数据会不断被刷新。这个过程中是通过DMA
控制的,不需要CPU
参与。我们只需读取ADC_Value
里面的数据即可得到 ADC 采集到的数据。其中ADC_Value[0]
为通道 0(PA0) 采集的数据,ADC_Value[1]
为通道 1(PA1) 采集的数据,ADC_Value[2]
为通道 0 采集的数据,如此类推。数组偶数下标的数据为通道 0 采集数据,数组奇数下标的数据为通道1采集数据。
在while(1) 循环中添加应用程序,将采集的数据装换为电压值并输出。
/* USER CODE BEGIN WHILE */
while (1)
{
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
HAL_Delay(500);
for(i = 0,ad1 =0,ad2=0; i < 100;)
{
ad1 += ADC_Value[i++];
ad2 += ADC_Value[i++];
}
ad1 /= 50;
ad2 /= 50;
printf("\r\n********ADC-DMA-Example********\r\n");
printf("[\tmain]info:AD1_value=%1.3fV\r\n", ad1*3.3f/4096);
printf("[\tmain]info:AD2_value=%1.3fV\r\n", ad2*3.3f/4096);
}
/* USER CODE END 3 */
程序中将数组偶数下标数据加起来求平均值,实现均值滤波的功能,再将数据装换为电压值,即为PA0管脚的电压值。同理对数组奇数下标数据处理得到PA1管脚的电压值。
同时ADC采样也可以采用我之前描述的采用定时器对其平滑滤波!
通常片上的ADC的精度往往达不到我们的要求,因为它的精度实在是太低了。有两个替代方案:
1、SDADC, 这个是STM32F373
上特有的功能,16位高速ADC,支持差分输入。掌握难度较大,我也没有很好的掌握,所以就不在此展示了。
2、ADS, 就是外置 ADC。在我们比赛前,我们一直调教的是 ADS1256 这款芯片,能做到 0.01mV 的精度!这类芯片只需要进行 SPI 通信操作,便可以获取 ADC 数据。
三、DAC数模转化
说实话,这两年的开发中,我还没有使用过 DAC 的功能。但是这个功能也十分简单,配置好引脚后,编写业务代码即可。
1、CubeMX相关配置
勾选DAC
中的OUT Configuration
, 其余配置为默认配置不需修改。
2、编写业务代码
//开启DAC转换
HAL_DAC_Start(&hdac, DAC_CHANNEL_2);
// 设置DAC的大小
HAL_DAC_SetValue(&hdac, DAC_CHANNEL_2, DAC_ALIGN_12B_R, 2048);
编译程序并下载到开发板。如果没有出错用万用表测管脚的电压为1.65V。
四、I2C/SPI
在开发中,使用到 I2C/SPI 的时候通常是与其他模块间的通信,例如:使用I2C与OLED
通信,使用 SPI 与ADS1256
通信。所以在此情况下,我们只需要在模块现有库函数的基础之上,做少量代码的移植即可。
1、CubeMX相关配置-I2C
直接使能 I2C 即可。
2、编写业务代码-I2C
//主机的发送
HAL_StatusTypeDef HAL_I2C_Master_Transmit(I2C_HandleTypeDef *hi2c, uint16_t DevAddress, uint8_t *pData, uint16_t Size, uint32_t Timeout);
//主机的接收
HAL_StatusTypeDef HAL_I2C_Master_Receive(I2C_HandleTypeDef *hi2c, uint16_t DevAddress, uint8_t *pData, uint16_t Size, uint32_t Timeout);
//从机的发送
HAL_StatusTypeDef HAL_I2C_Slave_Transmit(I2C_HandleTypeDef *hi2c, uint8_t * pData, uint16_t Size, uint32_t Timeout);
//从机的接收
HAL_StatusTypeDef HAL_I2C_Slave_Receive(I2C_HandleTypeDef *hi2c, uint8_t *pData, uint16_t Size, uint32_t Timeout);
3、OLED 的移植-I2C
3.1、原有库函数的代码
...
void I2C_Configuration(void)
{
I2C_InitTypeDef I2C_InitStructure;
GPIO_InitTypeDef GPIO_InitStructure;
RCC_APB1PeriphClockCmd(RCC_APB1Periph_I2C1,ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE);
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6 | GPIO_Pin_7;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_OD;
GPIO_Init(GPIOB, &GPIO_InitStructure);
I2C_DeInit(I2C1);
I2C_InitStructure.I2C_Mode = I2C_Mode_I2C;
I2C_InitStructure.I2C_DutyCycle = I2C_DutyCycle_2;
I2C_InitStructure.I2C_OwnAddress1 = 0x30;
I2C_InitStructure.I2C_Ack = I2C_Ack_Enable;
I2C_InitStructure.I2C_AcknowledgedAddress = I2C_AcknowledgedAddress_7bit;
I2C_InitStructure.I2C_ClockSpeed = 400000;
I2C_Cmd(I2C1, ENABLE);
I2C_Init(I2C1, &I2C_InitStructure);
}
void I2C_WriteByte(uint8_t addr,uint8_t data)
{
while(I2C_GetFlagStatus(I2C1, I2C_FLAG_BUSY));
I2C_GenerateSTART(I2C1, ENABLE);
while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT));
I2C_Send7bitAddress(I2C1, OLED_ADDRESS, I2C_Direction_Transmitter);
while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED));
I2C_SendData(I2C1, addr);
while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTED));
I2C_SendData(I2C1, data);
while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTED));
I2C_GenerateSTOP(I2C1, ENABLE);
}
void WriteCmd(unsigned char I2C_Command)
{
I2C_WriteByte(0x00, I2C_Command);
}
void WriteDat(unsigned char I2C_Data)
{
I2C_WriteByte(0x40, I2C_Data);
}
void OLED_Init(void)
{
DelayMs(100);
...
3.2、移植代码分析
I2C_Configuration
其实就是I2C的初始化函数,CubeMX会帮我们生成,所以直接删除
I2C_WriteByte
被接下来的两个函数依赖,但是HAL中有相应的函数,所以直接删除
WriteCmd
和WriteDat
改写成HAL库的方式
DelayMs
改为 HAL 库中的函数
3.3、移植后的代码
...
#include "i2c.h"
void WriteCmd(unsigned char I2C_Command)
{
HAL_I2C_Mem_Write(&hi2c1,OLED_ADDRESS,0x00,I2C_MEMADD_SIZE_8BIT,&I2C_Command,1,100);
}
void WriteDat(unsigned char I2C_Data)
{
HAL_I2C_Mem_Write(&hi2c1,OLED_ADDRESS,0x40,I2C_MEMADD_SIZE_8BIT,&I2C_Data,1,100);
}
void OLED_Init(void)
{
HAL_Delay(100);
...
4、CubeMX相关配置-SPI
使能SPI后,但是需要根据设备的不同做分频处理。
5、编写业务代码-SPI
//SPI的发送
HAL_StatusTypeDef HAL_SPI_Transmit(SPI_HandleTypeDef *hspi, uint8_t *pData,uint16_t Size, uint32_t Timeout);
//SPI的接收
HAL_StatusTypeDef HAL_SPI_Receive(SPI_HandleTypeDef *hspi, uint8_t *pData,uint16_t Size, uint32_t Timeout);
//SPI的发送和接收
HAL_StatusTypeDef HAL_SPI_TransmitReceive(SPI_HandleTypeDef *hspi, uint8_t *pTxData, uint8_t *pRxData, uint16_t Size,
uint32_t Timeout);
6、ADS1256 的移植-SPI
SPI的移植比I2C的移植要难并且复杂很多,基本上所有的函数都需要做大大小小的改动,建议大家尽量不要自己移植,最好是在网上找到相关的资源。
五、FLASH
FLASH 的操作,不需要使用Cube MX做任何的配置,只需要做编程操作即可。
1、相关定义
#define BaseAddress ((uint32_t)0x080E0000) // 操作FLAH基地址
//需根据自己单片机的型号进行修改
uint32_t paper_table[100] = {0} ;//需要写入FLAH中的第一张表
uint32_t pwm_table[100] = {0};//需要写入FLAH中的第二张表
uint32_t length_table = 0;
2、FLAH 的写入
注意:我接下来提供的例程是来自STM32F407
,不同板子间的FLASH_EraseInitTypeDef
可能不同。
HAL_FLASH_Unlock();
FLASH_EraseInitTypeDef f;
f.TypeErase = FLASH_TYPEERASE_SECTORS;
//F103中为FLASH_TYPEERASE_PAGES,即页擦除
f.Sector = FLASH_SECTOR_11;
//F103中为 f.PageAddress = BaseAddress 即开始操作的地址为BaseAddress
f.NbSectors = 1;
//F103中为 f.NbPages = x,即擦除x页
//设置PageError
uint32_t PageError = 0;
//调用擦除函数
HAL_FLASHEx_Erase(&f, &PageError);
//对FLASH烧写
for(int i = 0; i < 100; i++)
{
HAL_FLASH_Program(TYPEPROGRAM_WORD, (BaseAddress + 4 * i), paper_table[i]);
}
for(int i = 0; i < 100; i++)
{
HAL_FLASH_Program(TYPEPROGRAM_WORD, (BaseAddress + 400 + 4 * i), pwm_table[i]);
}
//锁住FLASH
HAL_FLASH_Lock();
3、FLAH 的读取
FLAH
的读取十分简单,只需要读取相应地址上的值即可。
for(int i = 0; i < 100; i++)
{
paper_table[i] = *(__IO uint32_t*) (BaseAddress + 4 * i);
}
for(int i = 0; i < 100; i++)
{
pwm_table[i] = *(__IO uint32_t*) (BaseAddress + 400 + 4 * i);
}
while(paper_table[length_table] != 0)
{
length_table++;
}
六、算法-排序
这两个模板,是我用了很久的,通过长时间的测试,我向你保证它是绝对的可靠!
1、快速排序
void quick_sort(int q[], int l, int r)
{
if (l >= r) return;
int i = l - 1, j = r + 1, x = q[l];
while (i < j){
do i ++ ; while (q[i] < x);
do j -- ; while (q[j] > x);
if (i < j) {
q[i] = q[i]^q[j];
q[j] = q[i]^q[j];
q[i] = q[i]^q[j];
}
else break;
}
quick_sort(q, l, j), quick_sort(q, j + 1, r);
}
2、归并排序
void merge_sort(int q[], int l, int r)
{
if (l >= r)
return;
int mid = l + r >> 1;
merge_sort(q, l, mid);
merge_sort(q, mid + 1, r);
int k = 0, i = l, j = mid + 1;
while (i <= mid && j <= r)
if (q[i] < q[j])
tmp[k ++ ] = q[i ++ ];
else
tmp[k ++ ] = q[j ++ ];
while (i <= mid)
tmp[k ++ ] = q[i ++ ];
while (j <= r)
tmp[k ++ ] = q[j ++ ];
for (i = l, j = 0; i <= r; i ++, j ++ )
q[i] = tmp[j];
}
3、算法-MPPT
在做最大功率点追踪,这个算法是十分重要的。我在这里分享一下我是怎么对其优化的,首先我写了一个能实现功能的最基础的版本。
4、基础版本
#include "mppt.h"
#include "main.h"
#include "usart.h"
//上一次的功率
double l_power = 0.0 ;
//功率上升或下降
int updown = 0;
//步长
int MPPT_STEP = 160;
/* 扰动法计算
*
*/
int mppt_po(double u, double i, int pwm)
{
double power = (u * i) < 0 ? 0 : u * i ;
printf("[Info]mppt_po:当前电流:%f,当前电压:%f,当前功率:%f\r\n", i, u, power);
if(power < l_power || power == 0)
{
updown ^= 1;
printf("[Info]mppt_po:当前功率:%f,小于此前功率:%f\r\n", power, l_power);
}
if(updown)
{
printf("[Info]mppt_po:PWM:%d调节为:%d\r\n", pwm, pwm + MPPT_STEP);
pwm += MPPT_STEP;
}
else
{
printf("[Info]mppt_po:PWM:%d调节为:%d\r\n", pwm, pwm - MPPT_STEP);
pwm -= MPPT_STEP;
}
printf("[Info]mppt_po:该次调节结束\r\n");
pwm = pwm < 0 ? 0 : (pwm >= 1599 ? 1599 : pwm);
l_power = power;
return pwm;
}
5、算法的不足与解决方案
1、这个算法一直在调节,这很有可能造成能量的损耗。解决方案:采用标志位,判断是否稳定。
2、这个算法步长不变,从头到尾固定步长。如果步长太长不精细,如果步长太短整体调节较慢。解决方案:采用可变步长。
3、使用三目运算符取代大量的if-else
4、Log信息采用条件编译的方法(很早之前写的,没能采用串口的终极解决方案,有点遗憾)
5、抽离变量,写出结构体,配以初始化函数。
6、最终版本
/**
* @file :mppt.c
*
* @brief: MPPT 最大功率点追踪
*
* @auther : Reyunn
*
*/
#include "mppt.h"
#include "main.h"
#include "usart.h"
extern void quick_sort(int q[], int l, int r);
typedef struct
{
double l_power;//上一次的功率
uint8_t updown;//功率上升或下降
int max_step;//步长
int min_step;//步长
int pwm_max ; //pwm最大值
int pwm_min ; //pwm最小值
uint8_t count; //计算微调次数
uint8_t state; //状态
uint8_t time; //改变方向次数
int l_pwm[10];
} MPPT;
MPPT mppt;
/**
* @brief mppt 初始化
* @param l_power : 上次测量功率
* @param updown : 上升或下降
* @param min_step :最小步长
* @param max_step :最大步长
* @param pwm_max : pwm最大值
* @param pwm_min : pwm最小值
* @retval None
*/
void mppt_init(double l_power, uint8_t updown, int min_step, int max_step, int
pwm_max, int pwm_min)
{
mppt.l_power = l_power;
mppt.updown = updown;
mppt.max_step = max_step;
mppt.min_step = min_step;
mppt.pwm_max = pwm_max;
mppt.pwm_min = pwm_min;
mppt.count = 0;
mppt.state = 1;
mppt.time = 0;
}
/**
* @brief 扰动法计算
* @param u : 当前电压值
* @param i :当前电流值
* @param pwm : 当前pwm值
* * @retval 计算后的pwm值
*/
int mppt_po(double u, double i, int pwm)
{
double power = (u * i) < 0 ? 0 : u * i ;
if(mppt.state)
{
#if Log
printf("[info]mppt_po:当前电流:%f,当前电压:%f,当前功率%f\r\n", i, u, power);
#endif
if(power < mppt.l_power || pwm == mppt.pwm_max || pwm == mppt.pwm_min)
{
mppt.updown ^= 1;
mppt.time ++;
if(mppt.time > 5 && power < mppt.l_power )
mppt.l_pwm[mppt.count++] = pwm;
#if Log
printf("[info]mppt_po:当前功率:%f,小于此前功率%f\r\n", power, mppt.l_power);
#endif
}
pwm = (mppt.updown == 1) ? ((mppt.count > 0) ? pwm + mppt.min_step : pwm +
mppt.max_step) : ( (mppt.count > 0 ) ? pwm - mppt.min_step : pwm - mppt.
max_step);
pwm = (pwm < mppt.pwm_min ) ? mppt.pwm_min : ( (pwm >= mppt.pwm_max) ? mppt.
pwm_max : pwm);
mppt.l_power = power;
if(mppt.count == 10)
{
mppt.state = 0;
quick_sort(mppt.l_pwm, 0, 9);
return ( mppt.l_pwm[5] + mppt.l_pwm[4] + mppt.l_pwm[3] + mppt.l_pwm[6] ) /
4;
}
return pwm;
} else
{
if( power - mppt.l_power > 1000 || power - mppt.l_power < -1000 )
{
mppt.count = 0;
mppt.state = 1;
mppt.time = 0;
#if Log
printf("[info]mppt.c:进入调整模式\r\n");
#endif
}
return pwm;
}
}
End
推荐好文 点击蓝色字体即可跳转
欢迎转发、留言、点赞、分享,感谢您的支持!